﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;

namespace SpiderSync
{   
    class SpiderSync : IDisposable
    {
        private readonly FileSystemWatcher _watcher = new FileSystemWatcher();
        private readonly SyncQueue _queue = new SyncQueue();

        private Thread _syncThread;

        public static List<String> excludePatterns { get; set; }

        private string _sourceDir;
        public string SourceDir
        {
            get { return this._sourceDir; }
            set { this._sourceDir = value; }
        }
        
        private string _destinationDir;
        public string DestinationDir
        {
            get { return this._destinationDir; }
            set { this._destinationDir = value; }
        }

        private int _delay = 1000;
        public int Delay
        {
            get { return this._delay; }
            set { this._delay = value; }
        }

        private int _retries = 10;
        public int Retries
        {
            get { return this._retries; }
            set { this._retries = value; }
        }

        private bool _verbose = false;
        public bool Verbose
        {
            get { return this._verbose; }
            set { this._verbose = value; }
        }

        private bool _skipInitialSync = false;
        public bool SkipInitialSync
        {
            get { return _skipInitialSync; }
            set { _skipInitialSync = value; }
        }
        

        public SpiderSync()
        {
            this._syncThread = new Thread(new ThreadStart(this.SyncThread));
        }

        ~SpiderSync()
        {
            this.Dispose();
        }

        public void Run()
        {
            this.CheckProperties();
            
            Output.Write("SpiderSync started: " + this._sourceDir + " -> " + this._destinationDir, MessageType.message);

            if (!this.SkipInitialSync)
            {
                this.DoReSync();
            }

            this._syncThread.Start();

            this.InitalizeWatcher();
            this._watcher.EnableRaisingEvents = true;
        }

        /// <summary>
        /// Check configuration properties
        /// </summary>
        private void CheckProperties()
        {
            if (this._sourceDir == null)
            {
                throw new Exception("Source directory specified.");
            }

            if (this._destinationDir == null)
            {
                throw new Exception("No destination directory specified.");
            }

            if (!FilesystemUtils.PathExists(this._sourceDir))
            {
                throw new Exception("Source directory '" + this._sourceDir + "' does not exists.");
            }

            if (!FilesystemUtils.PathExists(this._destinationDir))
            {
                throw new Exception("Destination directory '" + this._destinationDir + "' does not exists.");
            }

            if (!FilesystemUtils.IsDir(this._sourceDir))
            {
                throw new Exception("Source directory '" + this._sourceDir + "' is not a directory.");
            }

            if (!FilesystemUtils.IsDir(this._destinationDir))
            {
                throw new Exception("Destination directory '" + this._destinationDir + "' is not a directory.");
            }
        }
     
        /// <summary>
        /// Initalizes the filesystem watcher
        /// </summary>
        private void InitalizeWatcher()
        {
            this._watcher.IncludeSubdirectories = true;
            this._watcher.InternalBufferSize = 65534;
            this._watcher.Path = this._sourceDir;
            this._watcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName | NotifyFilters.LastWrite;

            this._watcher.Created += new FileSystemEventHandler(ChangeHandler);
            this._watcher.Changed += new FileSystemEventHandler(ChangeHandler);
            this._watcher.Deleted += new FileSystemEventHandler(ChangeHandler);
            this._watcher.Renamed += new RenamedEventHandler(ChangeHandler);
        }

        /// <summary>
        /// Handle filesystem change events
        /// </summary>
        /// <param name="source">Event source</param>
        /// <param name="eventArgs">EventArgs object</param>
        private void ChangeHandler(object source, object eventArgs)
        {
            if (!IsEventPathExcluded(eventArgs))
            {
                lock (this._queue)
                {
                    this._queue.Add(SyncItem.FromFilesystemEvent(eventArgs));
                    Monitor.Pulse(this._queue);
                }
            }
        }

        /// <summary>
        /// Check if specified event path matches any exluded pattern
        /// </summary>
        /// <param name="eventArgs">EventArgs object</param>
        /// <returns>True if excluded, fale otherwise</returns>
        private bool IsEventPathExcluded(object eventArgs)
        {
            String path = "";

            RenamedEventArgs renameEvent = eventArgs as RenamedEventArgs;
            if (renameEvent != null)
            {
                path = renameEvent.Name;
            }
            else
            {
                FileSystemEventArgs changeEvent = eventArgs as FileSystemEventArgs;
                if (changeEvent != null)
                {
                    path = changeEvent.Name;
                }
            }

            return path != "" && FilesystemUtils.IsMatchWildcards(excludePatterns, path);
        }

        /// <summary>
        /// Resync source and destination directory
        /// </summary>
        public void DoReSync()
        {
            Output.Write("Synchronizing folders...");
            FilesystemUtils.SyncDirectories(_sourceDir, _destinationDir, false, this.Retries);
            Output.Write("Folder synchronization done");
        }

        /// <summary>
        /// Synchronization thread method, synchronizes queued items
        /// </summary>
        private void SyncThread()
        {
            SyncItem item;

            while (true)            
            {
                Output.ShowIdle();
            
                lock (this._queue)
                {
                    Monitor.Wait(this._queue);
                }

                while (null != (item = this._queue.Top()))
                {
                    try
                    {
                        int delta = (int)DateTime.Now.Subtract(item.date).TotalMilliseconds;
                        if (delta <= this._delay)
                        {
                            Thread.Sleep(delta);
                        }

                        if (this.Verbose)
                        {
                            Output.Write("SYNC: " + item);
                        }
                        else
                        {
                            Output.PulseIndicator();
                        }
                        this.performSync(item, this._retries);
                    }
                    catch (Exception e)
                    {
                        Output.Write("Sync failed when tried to " + item + " , because" + e.Message, MessageType.warning);
                    }
                }
            }
        }

        /// <summary>
        /// Performs item synchronization with retries
        /// </summary>
        /// <param name="item">Item to sync</param>
        /// <param name="retries">Max retry count</param>
        private void performSync(SyncItem item, int retries)
        {
            try
            {
                item.DoSync(_sourceDir, _destinationDir, retries);
            }
            catch (DirectoryNotFoundException)
            {
                string destinationPath = item.GetDestinationPath(_destinationDir);
                if (!FilesystemUtils.PathExists(destinationPath))
                {
                    this.FixMissingDestination(destinationPath);
                    Console.SetCursorPosition(0, Console.CursorTop - 1);
                }
                else
                {
                    throw;
                }
            }
            catch (IOException e)
            {
                if (FilesystemUtils.IsFileLockedException(e) && retries-- > 0)
                {
                    Thread.Sleep(_delay);
                    this.performSync(item, retries);
                }
                else
                {
                    throw;
                }
            }
        }

        /// <summary>
        /// Finds the closest existing path of the specified path
        /// and resync with source folder
        /// </summary>
        /// <param name="destinationPath"></param>
        private void FixMissingDestination(string destinationPath)
        {
            destinationPath = FilesystemUtils.GetClosestExistingParentPath(destinationPath);
          
            string relativePath = FilesystemUtils.GetRelativePath(destinationPath, this._destinationDir);
            if (relativePath != null)
            {
                string sourcePath = Path.Combine(this._sourceDir, relativePath);
                FilesystemUtils.SyncDirectories(sourcePath, destinationPath, true, this.Retries);
            }
        }

        /// <summary>
        /// Cleanup
        /// </summary>
        public void Dispose()
        {
            this._watcher.Dispose();
            this._syncThread.Abort();
        }

    }
}
